import type { UploadProgressEvent } from 'element-plus';
import type { UploadRequestOptions } from 'element-plus/es/components/upload/src/upload';

import { useAppConfig } from '@croco/framework/hooks';
import { useUserStore } from '@croco/framework/stores';
import { axios } from '@croco/plugins/axios';

const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);

interface UploadProgress extends UploadProgressEvent {
    status: string;
}

interface RequestParams {
    [key: string]: boolean | number | string;
}

class ChunkUploadChain {
    private concurrency: number;
    private controller = new AbortController();
    private readonly loginUser: any;
    private md5 = '';
    private readonly options: UploadRequestOptions;
    private size: number;
    private total: number = 0;
    private readonly uid: number;
    private readonly url: string;

    constructor(action: string, options: UploadRequestOptions, size = 5 * 1024 * 1024, concurrency = 5) {
        this.uid = options.file.uid;
        this.url = `${apiURL}${action}`;
        this.options = options;
        this.size = size;
        this.concurrency = concurrency;
        const userStore = useUserStore();
        this.loginUser = userStore.loginUser;
    }

    // 链式起点
    static create(action: string, options: UploadRequestOptions) {
        return new ChunkUploadChain(action, options);
    }

    // 中止上传
    public abort() {
        this.controller.abort();
    }

    // 核心链式方法
    compute() {
        return new Promise<this>((resolve, reject) => {
            const worker = new Worker(new URL('md5.worker.js', import.meta.url));
            worker.postMessage({ file: this.options.file, size: this.size });
            // eslint-disable-next-line unicorn/prefer-add-event-listener
            worker.onmessage = ({ data }) => {
                if (this.controller.signal.aborted) {
                    worker.terminate();
                }

                if (data.status === 'computed') {
                    this.md5 = data.value;
                    this.total = data.total;
                    worker.terminate();
                    resolve(this);
                } else {
                    this.options.onProgress(data);
                }
            };

            // eslint-disable-next-line unicorn/prefer-add-event-listener
            worker.onerror = (e) => {
                worker.terminate();
                reject(e);
            };
        });
    }

    public getUid() {
        return this.uid;
    }

    async send() {
        const params = this.createParams();
        let sent = 0;
        // 校验断点
        const verify = await axios.get(this.url, { headers: this.getHeaders(false), params, signal: this.controller.signal });
        if (verify.data.code === 200) {
            // 分块并发上传
            const queue: FormData[] = [];
            for (let i = 0; i < this.total; i++) {
                // 跳过已上传文件块
                if (verify.data.exists?.includes(i)) {
                    sent++;
                } else {
                    // 组装FormData对象
                    const formData = new FormData();
                    Object.entries(params).forEach(([k, v]) => {
                        if (k === 'index') {
                            formData.append('index', String(i));
                        } else {
                            formData.append(k, v as any);
                        }
                    });
                    formData.append('file', this.getChunkBlob(i));
                    // 向队列添加上传分块
                    queue.push(formData);
                }

                // 并发上传
                if (queue.length === this.concurrency || i === this.total - 1) {
                    const resp = await Promise.all(queue.map((formData) => axios.post(this.url, formData, { headers: this.getHeaders(true), signal: this.controller.signal }))).finally(
                        () => {
                            queue.splice(0); // 清除队列
                        },
                    );
                    let fail;
                    resp.forEach(({ data }: any) => {
                        if (data.code === 200) {
                            sent++;
                            this.options.onProgress({ status: 'uploading', percent: Math.round((sent / this.total) * 100) } as UploadProgress);
                        } else {
                            fail = data;
                        }
                    });

                    // 一个文件块上传失败，整个文件直接退出上传
                    if (fail) {
                        this.options.onSuccess(fail);
                        break;
                    }
                }
            }
        } else {
            this.options.onSuccess(verify.data);
        }

        // 合并文件块
        if (sent === this.total) {
            this.options.onProgress({ status: 'merging' } as UploadProgress);
            const merge = await axios.put(this.url, params, { headers: this.getHeaders(false), signal: this.controller.signal });
            this.options.onSuccess(merge.data);
        }
    }

    // 配置方法
    withChunkSize(size: number) {
        this.size = size;
        return this;
    }

    withConcurrency(concurrency: number) {
        this.concurrency = Math.min(concurrency, 10);
        return this;
    }

    private createParams(): RequestParams {
        const file = this.options.file as any;
        const params = {
            id: this.md5,
            owner: this.loginUser.userId,
            name: file.name,
            size: file.size,
            index: 0,
            ...file.params,
        };
        // 目录上传
        if (file.webkitRelativePath) {
            params.relativePath = file.webkitRelativePath.slice(0, Math.max(0, file.webkitRelativePath.lastIndexOf('/')));
        }
        return params;
    }

    private getChunkBlob(index: number): Blob {
        const start = index * this.size;
        const end = Math.min(start + this.size, this.options.file.size);
        return this.options.file.slice(start, end);
    }

    private getHeaders(isUpload: boolean) {
        return isUpload
            ? {
                  Authorization: this.loginUser.token,
                  'Content-Type': 'multipart/form-data',
              }
            : {
                  Authorization: this.loginUser.token,
                  'Content-Type': 'application/x-www-form-urlencoded',
              };
    }
}

type UploadTask = {
    abort: () => void;
    run: () => void;
    uid: number;
};

// 多文件管理
class UploadManager {
    private readonly concurrency: number;
    private queue: Array<UploadTask> = [];
    private taskMap = new Map<number, UploadTask>();

    constructor(concurrency = 2) {
        this.concurrency = concurrency;
    }

    abort(uid?: number) {
        if (uid) {
            const task = this.taskMap.get(uid);
            if (task) {
                task.abort();
            }
        } else {
            // 先删除未开任务，再终止正在执行任务，终止任务时会触发异常
            this.queue = [];
            this.taskMap.values().forEach((task) => task.abort());
        }
    }

    add(uploader: ChunkUploadChain): Promise<void> {
        return new Promise((resolve, reject) => {
            const task = {
                uid: uploader.getUid(),
                run: async () => {
                    try {
                        const instance = await uploader.compute();
                        await instance.send();
                        resolve();
                    } catch (error) {
                        reject(error);
                    } finally {
                        this.taskMap.delete(uploader.getUid());
                        this.next();
                    }
                },
                abort: () => {
                    uploader.abort();
                },
            };

            if (this.taskMap.size < this.concurrency) {
                task.run();
                this.taskMap.set(task.uid, task);
            } else {
                this.queue.push(task);
            }
        });
    }

    private next() {
        if (this.queue.length > 0 && this.taskMap.size < this.concurrency) {
            const task = this.queue.shift();
            if (task) {
                task.run();
                this.taskMap.set(task.uid, task);
            }
        }
    }
}

// Vue Composition API 封装
export const useUploadManager = (chunkSize: number, concurrency: number, disorder = true) => {
    const manager = new UploadManager(concurrency);

    return {
        add: (action: string, options: UploadRequestOptions) => {
            const uploader = ChunkUploadChain.create(action, options)
                .withChunkSize(chunkSize)
                .withConcurrency(disorder ? concurrency * 2 : 1);
            manager.add(uploader);
            return uploader;
        },
        abort: (uid?: number) => manager.abort(uid),
    };
};
